package net.es.weathermap.http;

import java.awt.Dimension;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.es.weathermap.beans.AggrConnection;
import net.es.weathermap.beans.Connection;
import net.es.weathermap.beans.Domain;
import net.es.weathermap.beans.Hub;
import net.es.weathermap.beans.IfceConnection;
import net.es.weathermap.beans.Node;

import org.json.JSONException;
import org.json.JSONObject;

import edu.uci.ics.jung.algorithms.layout.CircleLayout;
import edu.uci.ics.jung.algorithms.layout.FRLayout;
import edu.uci.ics.jung.algorithms.layout.ISOMLayout;
import edu.uci.ics.jung.algorithms.layout.KKLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.SparseMultigraph;

import org.apache.log4j.*;

public class TopologyHandler {
    private static Logger LOG = Logger.getLogger(JSONServlet.class);
    private HashMap<Integer, ArrayList<Node>> allNodesByLod;
    private HashMap<Integer, ArrayList<Node>> geoNodesByLod;
    private HashMap<Integer, ArrayList<Node>> connectedNodesByLod;
    private HashMap<Integer, Graph<Node, Connection>> graphsByLod;
    private HashMap<String, Double> nodeLats;
    private HashMap<String, Double> nodeLngs;
    private LatLngInfo latLngInfo;
    private Dimension imageSize;
    private String layoutType;
    private Double gridUnit;
    private int zoom;

    public TopologyHandler() {
        this.allNodesByLod = new HashMap<Integer, ArrayList<Node>>();
        this.geoNodesByLod = new HashMap<Integer, ArrayList<Node>>();
        this.connectedNodesByLod = new HashMap<Integer, ArrayList<Node>>();
        this.graphsByLod = new HashMap<Integer, Graph<Node, Connection>>();
        this.latLngInfo = new LatLngInfo();
        this.nodeLats = new HashMap<String, Double>();
        this.nodeLngs = new HashMap<String, Double>();
    }

    public JSONObject getTopology(Domain domain, JSONObject topologyRequestJ)
        throws JSONException, IOException {

        // LOG.info(topologyRequestJ.toString(2));

        // layoutType: geo (default), isom, circle
        this.layoutType = "geo";
        if (topologyRequestJ != null && topologyRequestJ.has("layoutType")) {
            this.layoutType = topologyRequestJ.getString("layoutType");
        }
        // default bounding box is the globe
        this.latLngInfo.setLatNW(90.0D);
        this.latLngInfo.setLngNW(-180.0D);
        this.latLngInfo.setLatSE(-90.0D);
        this.latLngInfo.setLngSE(180.0D);

        // jump thru hoops to avoid null pointers
        if (topologyRequestJ != null && !this.layoutType.equals("geo")) {
            if (topologyRequestJ.has("latNW")) {
                this.latLngInfo.setLatNW(topologyRequestJ.getDouble("latNW"));
            }
            if (topologyRequestJ.has("lngNW")) {
                this.latLngInfo.setLngNW(topologyRequestJ.getDouble("lngNW"));
            }
            if (topologyRequestJ.has("latSE")) {
                this.latLngInfo.setLatSE(topologyRequestJ.getDouble("latSE"));
            }
            if (topologyRequestJ.has("lngSE")) {
                this.latLngInfo.setLngSE(topologyRequestJ.getDouble("lngSE"));
            }
        }
        /*
        LOG.info("latNW: "+this.latLngInfo.getLatNW()+
                " lngNW: "+this.latLngInfo.getLngNW()+
                " latSE: "+this.latLngInfo.getLatSE()+
                " lngSE: "+this.latLngInfo.getLngSE());
        */
        this.latLngInfo.setLatDelta();
        this.latLngInfo.setLngDelta();
        Double tmpLatDelta = this.latLngInfo.getLatDelta();
        Double tmpLngDelta = this.latLngInfo.getLngDelta();
        this.latLngInfo.setLatCenter();
        this.latLngInfo.setLngCenter();

        // need to zoom in to make sure that the image dimensions passed to
        // the jung layout are big enough. they are in pixels.
        this.zoom = 0;
        while (tmpLatDelta < 1000.0D && tmpLngDelta < 1000.0D) {
            tmpLatDelta = tmpLatDelta * 10.0D;
            tmpLngDelta = tmpLngDelta * 10.0D;
            this.zoom++;
        }
        /*
        LOG.info("latDelta: "+this.latLngInfo.getLatDelta()+
                " lonDelta: "+this.latLngInfo.getLngDelta());
        */
        this.imageSize = new Dimension(tmpLngDelta.intValue(), tmpLatDelta.intValue());

        this.geoNodesByLod.put(0, new ArrayList<Node>());
        this.geoNodesByLod.put(1, new ArrayList<Node>());
        this.geoNodesByLod.put(2, new ArrayList<Node>());

        this.connectedNodesByLod.put(0, new ArrayList<Node>());
        this.connectedNodesByLod.put(1, new ArrayList<Node>());
        this.connectedNodesByLod.put(2, new ArrayList<Node>());

        this.allNodesByLod.put(0, new ArrayList<Node>());
        this.allNodesByLod.put(1, new ArrayList<Node>());
        this.allNodesByLod.put(2, new ArrayList<Node>());

        // decide what will be drawn
        // currently is all nodes that are in the bounding box plus any other
        // nodes directly connected to any of the ones in the box
        this.clipToBoundingBox(domain);

        JSONObject resultJ = new JSONObject();
        for (String type : new String[] { Connection.IP, Connection.SDN, Connection.PEER } ) {
            JSONObject topoJ = new JSONObject();
            for (int lod = 0; lod <= 2; lod++) {
                this.addEdges(type, lod);
            }
            HashMap<Node, HashMap<String, Double>> borderCoordsByNode = new HashMap<Node, HashMap<String, Double>>();

            for (int lod = 0; lod <= 2; lod++) {
                this.addBorderInfo(lod, borderCoordsByNode);
            }
            for (int lod = 0; lod <= 2; lod++) {
                JSONObject lodJ = new JSONObject();
                this.setJSONNodes(lodJ, lod, borderCoordsByNode, type);
                topoJ.put(""+lod, lodJ);
            }
            resultJ.put(type, topoJ);
        }
        this.gridUnit = Math.abs(this.latLngInfo.getLngDelta() / 20.0D);
        if (this.layoutType.equals("geo")) {
            this.gridUnit = 0.05D;
        }
        HashMap<String, Integer> parallelHelp = new HashMap<String, Integer>();
        for (String type : new String[] { Connection.IP, Connection.SDN, Connection.PEER } ) {
            JSONObject topoJ = (JSONObject) resultJ.get(type);
            for (int lod = 0; lod <= 2; lod++) {
                this.setJSONNodeCoords(topoJ, lod);
                this.setJSONConnIndices(topoJ, lod, parallelHelp);
            }
        }
        return resultJ;
    }

    // currently all hubs and nodes are chosen; this just adds them
    private void clipToBoundingBox(Domain domain) {

        for (Hub hub : domain.getHubs()) {
            if (isInRectangle(hub.getLatitude(), hub.getLongitude(), this.latLngInfo)) {
                if (!this.geoNodesByLod.get(0).contains(hub)) {
                    this.geoNodesByLod.get(0).add(hub);
                }
                if (!this.allNodesByLod.get(0).contains(hub)) {
                    this.allNodesByLod.get(0).add(hub);
                }
            }
            for (Node node: hub.getNodes()) {
                if (isInRectangle(node.getLatitude(), node.getLongitude(), this.latLngInfo)) {
                    if (!this.geoNodesByLod.get(1).contains(node)) {
                        this.geoNodesByLod.get(1).add(node);
                    }
                    if (!this.geoNodesByLod.get(2).contains(node)) {
                        this.geoNodesByLod.get(2).add(node);
                    }
                    if (!this.allNodesByLod.get(1).contains(node)) {
                        this.allNodesByLod.get(1).add(node);
                    }
                    if (!this.allNodesByLod.get(2).contains(node)) {
                        this.allNodesByLod.get(2).add(node);
                    }
                }
            }
        }
    }

    private void addEdges(String type, int lod) {

        ArrayList<Node> nodes = this.geoNodesByLod.get(lod);
        Graph<Node, Connection> graph;
        if (this.graphsByLod.get(lod) == null) {
             graph = new SparseMultigraph<Node, Connection>();
             this.graphsByLod.put(lod, graph);
        } else {
            graph = this.graphsByLod.get(lod);
        }
        for (Node node : nodes) {
            if (lod == 2) {
                for (Connection conn : node.getIfceConnections(type)) {
                    if (!graph.getVertices().contains(node)) {
                        graph.addEdge(new Connection(), node, node);
                    }
                    if (!this.geoNodesByLod.get(lod).contains(conn.getDst())) {
                        if (!this.connectedNodesByLod.get(lod).contains(conn.getDst())) {
                            this.connectedNodesByLod.get(lod).add(conn.getDst());
                            if (!this.allNodesByLod.get(lod).contains(conn.getDst())) {
                                this.allNodesByLod.get(lod).add(conn.getDst());
                            }
                        }
                    } else {
                        graph.addEdge(conn, node, conn.getDst());
                    }
                }
            } else {
                for (Connection conn : node.getAggrConnections(type)) {
                    if (!graph.getVertices().contains(node)) {
                        graph.addEdge(new Connection(), node, node);
                    }
                    if (!this.geoNodesByLod.get(lod).contains(conn.getDst())) {
                        if (!this.geoNodesByLod.get(lod).contains(conn.getDst())) {
                            if (!this.connectedNodesByLod.get(lod).contains(conn.getDst())) {
                                this.connectedNodesByLod.get(lod).add(conn.getDst());
                                if (!this.allNodesByLod.get(lod).contains(conn.getDst())) {
                                    this.allNodesByLod.get(lod).add(conn.getDst());
                                }
                            }
                        }
                   } else {
                       graph.addEdge(conn, node, conn.getDst());
                   }
                }
            }
        }
    }

    private void
        addBorderInfo(int lod,
                      HashMap<Node, HashMap<String, Double>> borderCoordsByNode) {

        HashMap<String, ArrayList<Node>> byBorder =
            new HashMap<String, ArrayList<Node>>();
        byBorder.put("south", new ArrayList<Node>());
        byBorder.put("north", new ArrayList<Node>());
        byBorder.put("east", new ArrayList<Node>());
        byBorder.put("west", new ArrayList<Node>());

        for (Node node : this.connectedNodesByLod.get(lod)) {
            Double lat = node.getLatitude();
            Double lng = node.getLongitude();
            Double thisLatDelta = this.latLngInfo.getLatCenter() - lat;
            Double thisLngDelta = this.latLngInfo.getLngCenter() - lng;
            if (Math.abs(thisLatDelta) > Math.abs(thisLngDelta)) {
                // node is somewhere roughly north or south of map
                if (thisLatDelta > 0) {
                    byBorder.get("south").add(node);
                } else {
                    byBorder.get("north").add(node);
                }
            } else {
                // node is somewhere roughly east or west of map
                if (thisLngDelta > 0) {
                    byBorder.get("east").add(node);
                } else {
                    byBorder.get("west").add(node);
                }
            }
        }
        this.setBorderCoords(byBorder, borderCoordsByNode);
    }

    private void setBorderCoords(
        HashMap<String, ArrayList<Node>> byBorder,
        HashMap<Node, HashMap<String, Double>> borderCoordsByNode) { 

        HashMap<String, Double> borderLines = new HashMap<String, Double>();
        borderLines.put("north", this.latLngInfo.getLatNW() - (0.1 * this.latLngInfo.getLatDelta()));
        borderLines.put("south", this.latLngInfo.getLatSE() + (0.1 * this.latLngInfo.getLatDelta()));
        borderLines.put("east",  this.latLngInfo.getLngNW() + (0.1 * this.latLngInfo.getLngDelta()));
        borderLines.put("west",  this.latLngInfo.getLngSE() - (0.1 * this.latLngInfo.getLngDelta()));

        for (String border : new String[] {"north", "south", "east", "west"} ) {
            ArrayList<Node> borderNodes = byBorder.get(border);
            Double moveUnit;
            if (border.equals("north") || border.equals("south")) {
                moveUnit = this.latLngInfo.getLngDelta() / (borderNodes.size() + 2);
            } else {
                moveUnit = this.latLngInfo.getLatDelta() / (borderNodes.size() + 2);
            }
            int i = 0;
            int j = 1;
            for (Node node : borderNodes) {
                Double newLat = 0.0D;
                Double newLng = 0.0D;
                Double moveAmount;
                if (i == 0) {
                    moveAmount = 0.D;
                } else if ( (i % 2) > 0 ) {
                    moveAmount = moveUnit * j;
                } else {
                    moveAmount = -moveUnit * j;
                    j++;
                }
                i++;
                if (border.equals("north") || border.equals("south")) {
                    newLat = borderLines.get(border);
                    newLng = this.latLngInfo.getLngCenter() + moveAmount;
                } else if (border.equals("east") || border.equals("west")) {
                    newLng = borderLines.get(border);
                    newLat = this.latLngInfo.getLatCenter() + moveAmount;
                }
                HashMap<String, Double> coords = new HashMap<String, Double>();
                borderCoordsByNode.put(node, coords);
                coords.put("latitude", newLat);
                coords.put("longitude", newLng);
            }
        }
    }

    private void setJSONNodes(JSONObject lodJ, int lod,
            HashMap<Node, HashMap<String, Double>> borderCoordsByNode,
            String type) throws JSONException {

        Graph<Node, Connection> graph = this.graphsByLod.get(lod);
        Layout<Node, Connection> layout = this.setLayout(graph);
        JSONObject nodesJ = new JSONObject();
        JSONObject connsJ = new JSONObject();
        lodJ.put("connections", connsJ);
        lodJ.put("nodes", nodesJ);

        for (Node node : this.allNodesByLod.get(lod)) {
            // special case for when only want to display routers with
            // international peers
            if ((lod != 2) && (type.equals(Connection.PEER) &&
                (node.getAggrConnections(Connection.PEER).isEmpty()))) {
                continue;
            }
            boolean addNode = false;
            JSONObject nodeJ = new JSONObject();
            if (this.geoNodesByLod.get(lod).contains(node)) {
                if (this.layoutType.equals("geo")) {
                    nodeJ.put("latitude", node.getLatitude());
                    nodeJ.put("longitude", node.getLongitude());
                    addNode = true;
                    this.nodeLats.put(node.getTopoId(), node.getLatitude());
                    this.nodeLngs.put(node.getTopoId(), node.getLongitude());
                } else {
                    if (this.geoNodesByLod.get(lod).size() <= 1) {
                        nodeJ.put("latitude", node.getLatitude());
                        nodeJ.put("longitude", node.getLongitude());
                        this.nodeLats.put(node.getTopoId(), node.getLatitude());
                        this.nodeLngs.put(node.getTopoId(), node.getLongitude());
                    } else {
                        Point2D coords = layout.transform(node);
                        Double x = coords.getX()* 0.9D;
                        Double y = coords.getY()* 0.7D;
                        Double newLng =
                            this.latLngInfo.getLngNW() +
                            Math.abs(this.latLngInfo.getLngDelta()*0.05D) +
                            (x / (Math.pow(10.0D, this.zoom)) );
                        Double newLat =
                            this.latLngInfo.getLatSE() +
                            Math.abs(this.latLngInfo.getLatDelta()*0.15D) +
                             (y / (Math.pow(10.0D, this.zoom)) );
                        nodeJ.put("latitude", newLat);
                        nodeJ.put("longitude", newLng);
                        this.nodeLats.put(node.getTopoId(), newLat);
                        this.nodeLngs.put(node.getTopoId(), newLng);
                    }
                }
            } else if (this.connectedNodesByLod.get(lod).contains(node)) {
                HashMap<String, Double> coords = borderCoordsByNode.get(node);
                nodeJ.put("latitude", coords.get("latitude"));
                nodeJ.put("longitude", coords.get("longitude"));
                this.nodeLats.put(node.getTopoId(), coords.get("latitude"));
                this.nodeLngs.put(node.getTopoId(), coords.get("longitude"));
            }
            nodeJ.put("name", node.getTopoId());
            if (lod == 0) {
                Hub hub = (Hub) node;
                List<Node> hubNodes = hub.getNodes();
                if (hubNodes.size() == 1) {
                    nodeJ.put("type", hubNodes.get(0).getType());
                } else {
                    int siteCtr = 0;
                    int peerCtr = 0;
                    for (Node hubNode: hubNodes) {
                        if (hubNode.getType().equals("site")) {
                            siteCtr++;
                        } else if (hubNode.getType().equals("peer")) {
                            peerCtr++;
                        }
                    }
                    if ((siteCtr == 0) && (peerCtr == 0)) {
                        nodeJ.put("type", "hub");
                    } else if (siteCtr == hubNodes.size()) {
                        nodeJ.put("type", "siteAggr");
                    } else if (peerCtr == hubNodes.size()) {
                        nodeJ.put("type", "peerAggr");
                    } else {
                        // assuming peer will never be present with sites or
                        // routers in aggregate
                        nodeJ.put("type", "siteRouterAggr");
                    }
                }
                for (AggrConnection aggrConn : node.getAggrConnections(type)) {
                    if (this.geoNodesByLod.get(lod).contains(aggrConn.getDst()) ||
                       (this.connectedNodesByLod.get(lod).contains(aggrConn.getDst())  &&
                       !this.connectedNodesByLod.get(lod).contains(aggrConn.getSrc()) )
                    ) {
                        addNode = true;
                        if (connsJ.has(aggrConn.getReverse().getTopoId())) {
                            continue;
                        }
                        JSONObject aggrConnJ = new JSONObject();
                        aggrConnJ.put("name", aggrConn.getTopoId());
                        aggrConnJ.put("dst", aggrConn.getDst().getTopoId());
                        aggrConnJ.put("src", aggrConn.getSrc().getTopoId());
                        aggrConnJ.put("capacity", aggrConn.getCapacity());
                        aggrConnJ.put("reverse", aggrConn.getReverse().getTopoId());
                        aggrConnJ.put("type", "aggregate");
                        connsJ.put(aggrConn.getTopoId(), aggrConnJ);
                    }
                }
            } else if (lod == 1) {
                nodeJ.put("type", node.getType());
                for (AggrConnection aggrConn : node.getAggrConnections(type)) {
                    if (this.geoNodesByLod.get(lod).contains(aggrConn.getDst()) ||
                        (this.connectedNodesByLod.get(lod).contains(aggrConn.getDst())  &&
                        !this.connectedNodesByLod.get(lod).contains(aggrConn.getSrc()) )
                    ) {
                        addNode = true;
                        if (connsJ.has(aggrConn.getReverse().getTopoId())) {
                            continue;
                        }
                        JSONObject aggrConnJ = new JSONObject();
                        aggrConnJ.put("name", aggrConn.getTopoId());
                        aggrConnJ.put("dst", aggrConn.getDst().getTopoId());
                        aggrConnJ.put("src", aggrConn.getSrc().getTopoId());
                        aggrConnJ.put("capacity", aggrConn.getCapacity());
                        aggrConnJ.put("reverse", aggrConn.getReverse().getTopoId());
                        aggrConnJ.put("type", "aggregate");
                        connsJ.put(aggrConn.getTopoId(), aggrConnJ);
                    }
                }
            } else if (lod == 2) {
                nodeJ.put("type", node.getType());
                for (IfceConnection ifceConn : node.getIfceConnections(type)) {
                    if (this.geoNodesByLod.get(lod).contains(ifceConn.getDst()) ||
                       (this.connectedNodesByLod.get(lod).contains(ifceConn.getDst())  &&
                       !this.connectedNodesByLod.get(lod).contains(ifceConn.getSrc()) )
                    ) {
                        addNode = true;
                        JSONObject ifceConnJ = new JSONObject();
                        if (connsJ.has(ifceConn.getReverse().getTopoId())) {
                            continue;
                        }
                        ifceConnJ.put("name", ifceConn.getTopoId());
                        ifceConnJ.put("src", ifceConn.getSrc().getTopoId());
                        ifceConnJ.put("dst", ifceConn.getDst().getTopoId());
                        ifceConnJ.put("capacity", ifceConn.getCapacity());
                        ifceConnJ.put("reverse", ifceConn.getReverse().getTopoId());
                        ifceConnJ.put("type", "interface");
                        connsJ.put(ifceConn.getTopoId(), ifceConnJ);
                    }
                }
            }
            if (addNode) {
                nodesJ.put(node.getTopoId(), nodeJ);
            }
        }
    }

    private void setJSONNodeCoords(JSONObject topoJ, int lod)
           throws JSONException {

        JSONObject lodJ = (JSONObject) topoJ.get(""+lod);
        JSONObject nodesJ = (JSONObject) lodJ.get("nodes");
        Iterator<String> nodeNamesIt = nodesJ.keys();
        while (nodeNamesIt.hasNext()) {
            String nodeName = (String) nodeNamesIt.next();
            JSONObject nodeJ = (JSONObject) nodesJ.get(nodeName);
            nodeJ.put("latitude", this.nodeLats.get(nodeName));
            nodeJ.put("longitude", this.nodeLngs.get(nodeName));
        }
    }

    private void setJSONConnIndices(JSONObject topoJ, int lod,
           HashMap<String, Integer> parallelHelp) throws JSONException {

        JSONObject lodJ = (JSONObject) topoJ.get(""+lod);
        JSONObject connsJ = (JSONObject) lodJ.get("connections");
        // NOTE: whenever you do connsJ.keys() you get a new iterator
        Iterator<String> connsNamesIt = connsJ.keys();
        while (connsNamesIt.hasNext()) {
            String connName = (String) connsNamesIt.next();
            // LOG.info(connName);
            JSONObject connJ = (JSONObject) connsJ.get(connName);
            if (connJ.has("idx")) {
                continue;
            }
            String name = connJ.getString("name");
            String src = connJ.getString("src");
            String dst = connJ.getString("dst");

            Double srcRelLat = this.nodeLats.get(src) - this.latLngInfo.getLatSE();
            Double srcRelLng = this.nodeLngs.get(src) - this.latLngInfo.getLngSE();
            Double dstRelLat = this.nodeLats.get(dst) - this.latLngInfo.getLatSE();
            Double dstRelLng = this.nodeLngs.get(dst) - this.latLngInfo.getLngSE();
            Double srcLatSq = Math.abs(srcRelLat/this.gridUnit);
            Double srcLngSq = Math.abs(srcRelLng/this.gridUnit);
            Double dstLatSq = Math.abs(dstRelLat/this.gridUnit);
            Double dstLngSq = Math.abs(dstRelLng/this.gridUnit);
            String srcSquare = srcLatSq.intValue()+"_"+srcLngSq.intValue();
            String dstSquare = dstLatSq.intValue()+"_"+dstLngSq.intValue();
            String tofrom = srcSquare+"___"+dstSquare+"___"+lod;
            String fromto = dstSquare+"___"+srcSquare+"___"+lod;
            if (parallelHelp.get(tofrom) == null || parallelHelp.get(fromto) == null) {
                parallelHelp.put(tofrom, 1);
                parallelHelp.put(fromto, 1);
                connJ.put("idx", 1);
            } else {
                Integer idx = parallelHelp.get(fromto);
                Integer newidx = idx + 1;
                parallelHelp.put(fromto, newidx);
                parallelHelp.put(tofrom, newidx);
                connJ.put("idx", newidx);
                // LOG.info(name+" "+tofrom+" "+newidx);
            }
        }
    }

    private Layout<Node, Connection> setLayout(Graph<Node, Connection> graph) {

        Layout<Node, Connection> layout;
        if (this.layoutType != null && this.layoutType.equals("circle")) {
            layout = new CircleLayout<Node, Connection>(graph);
        } else if (this.layoutType != null && this.layoutType.equals("isom")) {
            layout = new ISOMLayout<Node, Connection>(graph);
        } else if (this.layoutType != null && this.layoutType.equals("fr")) {
            layout = new FRLayout<Node, Connection>(graph);
        } else {
            KKLayout<Node,Connection> tmpLayout = new KKLayout<Node, Connection>(graph);
            layout = (KKLayout<Node,Connection>) tmpLayout;
        }
        layout.setSize(this.imageSize);
        return layout;
    }

    private static boolean isInRectangle(Double lat, Double lon, LatLngInfo tl) {
        if ((lat <= tl.getLatNW() && lat >= tl.getLatSE()) &&
             (lon <= tl.getLngSE() && lon >= tl.getLngNW())) {
            return true;
        }
        return false;
    }

}
